// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE

(function(mod) {
  if (typeof exports == 'object' && typeof module == 'object')
    // CommonJS
    mod(
      require('../../lib/codemirror'),
      require('../xml/xml'),
      require('../meta'),
    );
  else if (typeof define == 'function' && define.amd)
    // AMD
    define(['../../lib/codemirror', '../xml/xml', '../meta'], mod);
  // Plain browser env
  else mod(CodeMirror);
})(function(CodeMirror) {
  'use strict';

  CodeMirror.defineMode(
    'markdown',
    function(cmCfg, modeCfg) {
      var htmlMode = CodeMirror.getMode(cmCfg, 'text/html');
      var htmlModeMissing = htmlMode.name == 'null';

      function getMode(name) {
        if (CodeMirror.findModeByName) {
          var found = CodeMirror.findModeByName(name);
          if (found) name = found.mime || found.mimes[0];
        }
        var mode = CodeMirror.getMode(cmCfg, name);
        return mode.name == 'null' ? null : mode;
      }

      // Should characters that affect highlighting be highlighted separate?
      // Does not include characters that will be output (such as `1.` and `-` for lists)
      if (modeCfg.highlightFormatting === undefined)
        modeCfg.highlightFormatting = false;

      // Maximum number of nested blockquotes. Set to 0 for infinite nesting.
      // Excess `>` will emit `error` token.
      if (modeCfg.maxBlockquoteDepth === undefined)
        modeCfg.maxBlockquoteDepth = 0;

      // Turn on task lists? ("- [ ] " and "- [x] ")
      if (modeCfg.taskLists === undefined) modeCfg.taskLists = false;

      // Turn on strikethrough syntax
      if (modeCfg.strikethrough === undefined) modeCfg.strikethrough = false;

      if (modeCfg.emoji === undefined) modeCfg.emoji = false;

      if (modeCfg.fencedCodeBlockHighlighting === undefined)
        modeCfg.fencedCodeBlockHighlighting = true;

      if (modeCfg.xml === undefined) modeCfg.xml = true;

      // Allow token types to be overridden by user-provided token types.
      if (modeCfg.tokenTypeOverrides === undefined)
        modeCfg.tokenTypeOverrides = {};

      var tokenTypes = {
        header: 'header',
        code: 'comment',
        quote: 'quote',
        list1: 'variable-2',
        list2: 'variable-3',
        list3: 'keyword',
        hr: 'hr',
        image: 'image',
        imageAltText: 'image-alt-text',
        imageMarker: 'image-marker',
        formatting: 'formatting',
        linkInline: 'link',
        linkEmail: 'link',
        linkText: 'link',
        linkHref: 'string',
        em: 'em',
        strong: 'strong',
        strikethrough: 'strikethrough',
        emoji: 'builtin',
      };

      for (var tokenType in tokenTypes) {
        if (
          tokenTypes.hasOwnProperty(tokenType) &&
          modeCfg.tokenTypeOverrides[tokenType]
        ) {
          tokenTypes[tokenType] = modeCfg.tokenTypeOverrides[tokenType];
        }
      }

      var hrRE = /^([*\-_])(?:\s*\1){2,}\s*$/,
        listRE = /^(?:[*\-+]|^[0-9]+([.)]))\s+/,
        taskListRE = /^\[(x| )\](?=\s)/i, // Must follow listRE
        atxHeaderRE = modeCfg.allowAtxHeaderWithoutSpace
          ? /^(#+)/
          : /^(#+)(?: |$)/,
        setextHeaderRE = /^ *(?:\={1,}|-{1,})\s*$/,
        textRE = /^[^#!\[\]*_\\<>` "'(~:]+/,
        fencedCodeRE = /^(~~~+|```+)[ \t]*([\w+#-]*)[^\n`]*$/,
        linkDefRE = /^\s*\[[^\]]+?\]:\s*\S+(\s*\S*\s*)?$/, // naive link-definition
        punctuation = /[!\"#$%&\'()*+,\-\.\/:;<=>?@\[\\\]^_`{|}~—]/,
        expandedTab = '    '; // CommonMark specifies tab as 4 spaces

      function switchInline(stream, state, f) {
        state.f = state.inline = f;
        return f(stream, state);
      }

      function switchBlock(stream, state, f) {
        state.f = state.block = f;
        return f(stream, state);
      }

      function lineIsEmpty(line) {
        return !line || !/\S/.test(line.string);
      }

      // Blocks

      function blankLine(state) {
        // Reset linkTitle state
        state.linkTitle = false;
        // Reset EM state
        state.em = false;
        // Reset STRONG state
        state.strong = false;
        // Reset strikethrough state
        state.strikethrough = false;
        // Reset state.quote
        state.quote = 0;
        // Reset state.indentedCode
        state.indentedCode = false;
        if (state.f == htmlBlock) {
          state.f = inlineNormal;
          state.block = blockNormal;
        }
        // Reset state.trailingSpace
        state.trailingSpace = 0;
        state.trailingSpaceNewLine = false;
        // Mark this line as blank
        state.prevLine = state.thisLine;
        state.thisLine = { stream: null };
        return null;
      }

      function blockNormal(stream, state) {
        var firstTokenOnLine = stream.column() === state.indentation;
        var prevLineLineIsEmpty = lineIsEmpty(state.prevLine.stream);
        var prevLineIsIndentedCode = state.indentedCode;
        var prevLineIsHr = state.prevLine.hr;
        var prevLineIsList = state.list !== false;
        var maxNonCodeIndentation =
          (state.listStack[state.listStack.length - 1] || 0) + 3;

        state.indentedCode = false;

        var lineIndentation = state.indentation;
        // compute once per line (on first token)
        if (state.indentationDiff === null) {
          state.indentationDiff = state.indentation;
          if (prevLineIsList) {
            state.list = null;
            // While this list item's marker's indentation is less than the deepest
            //  list item's content's indentation,pop the deepest list item
            //  indentation off the stack, and update block indentation state
            while (
              lineIndentation < state.listStack[state.listStack.length - 1]
            ) {
              state.listStack.pop();
              if (state.listStack.length) {
                state.indentation = state.listStack[state.listStack.length - 1];
                // less than the first list's indent -> the line is no longer a list
              } else {
                state.list = false;
              }
            }
            if (state.list !== false) {
              state.indentationDiff =
                lineIndentation - state.listStack[state.listStack.length - 1];
            }
          }
        }

        // not comprehensive (currently only for setext detection purposes)
        var allowsInlineContinuation =
          !prevLineLineIsEmpty &&
          !prevLineIsHr &&
          !state.prevLine.header &&
          (!prevLineIsList || !prevLineIsIndentedCode) &&
          !state.prevLine.fencedCodeEnd;

        var isHr =
          (state.list === false || prevLineIsHr || prevLineLineIsEmpty) &&
          state.indentation <= maxNonCodeIndentation &&
          stream.match(hrRE);

        var match = null;
        if (
          state.indentationDiff >= 4 &&
          (prevLineIsIndentedCode ||
            state.prevLine.fencedCodeEnd ||
            state.prevLine.header ||
            prevLineLineIsEmpty)
        ) {
          stream.skipToEnd();
          state.indentedCode = true;
          return tokenTypes.code;
        } else if (stream.eatSpace()) {
          return null;
        } else if (
          firstTokenOnLine &&
          state.indentation <= maxNonCodeIndentation &&
          (match = stream.match(atxHeaderRE)) &&
          match[1].length <= 6
        ) {
          state.quote = 0;
          state.header = match[1].length;
          state.thisLine.header = true;
          if (modeCfg.highlightFormatting) state.formatting = 'header';
          state.f = state.inline;
          return getType(state);
        } else if (
          state.indentation <= maxNonCodeIndentation &&
          stream.eat('>')
        ) {
          state.quote = firstTokenOnLine ? 1 : state.quote + 1;
          if (modeCfg.highlightFormatting) state.formatting = 'quote';
          stream.eatSpace();
          return getType(state);
        } else if (
          !isHr &&
          !state.setext &&
          firstTokenOnLine &&
          state.indentation <= maxNonCodeIndentation &&
          (match = stream.match(listRE))
        ) {
          var listType = match[1] ? 'ol' : 'ul';

          state.indentation = lineIndentation + stream.current().length;
          state.list = true;
          state.quote = 0;

          // Add this list item's content's indentation to the stack
          state.listStack.push(state.indentation);

          if (modeCfg.taskLists && stream.match(taskListRE, false)) {
            state.taskList = true;
          }
          state.f = state.inline;
          if (modeCfg.highlightFormatting)
            state.formatting = ['list', 'list-' + listType];
          return getType(state);
        } else if (
          firstTokenOnLine &&
          state.indentation <= maxNonCodeIndentation &&
          (match = stream.match(fencedCodeRE, true))
        ) {
          state.quote = 0;
          state.fencedEndRE = new RegExp(match[1] + '+ *$');
          // try switching mode
          state.localMode =
            modeCfg.fencedCodeBlockHighlighting && getMode(match[2]);
          if (state.localMode)
            state.localState = CodeMirror.startState(state.localMode);
          state.f = state.block = local;
          if (modeCfg.highlightFormatting) state.formatting = 'code-block';
          state.code = -1;
          return getType(state);
          // SETEXT has lowest block-scope precedence after HR, so check it after
          //  the others (code, blockquote, list...)
        } else if (
          // if setext set, indicates line after ---/===
          state.setext ||
          // line before ---/===
          ((!allowsInlineContinuation || !prevLineIsList) &&
            !state.quote &&
            state.list === false &&
            !state.code &&
            !isHr &&
            !linkDefRE.test(stream.string) &&
            (match = stream.lookAhead(1)) &&
            (match = match.match(setextHeaderRE)))
        ) {
          if (!state.setext) {
            state.header = match[0].charAt(0) == '=' ? 1 : 2;
            state.setext = state.header;
          } else {
            state.header = state.setext;
            // has no effect on type so we can reset it now
            state.setext = 0;
            stream.skipToEnd();
            if (modeCfg.highlightFormatting) state.formatting = 'header';
          }
          state.thisLine.header = true;
          state.f = state.inline;
          return getType(state);
        } else if (isHr) {
          stream.skipToEnd();
          state.hr = true;
          state.thisLine.hr = true;
          return tokenTypes.hr;
        } else if (stream.peek() === '[') {
          return switchInline(stream, state, footnoteLink);
        }

        return switchInline(stream, state, state.inline);
      }

      function htmlBlock(stream, state) {
        var style = htmlMode.token(stream, state.htmlState);
        if (!htmlModeMissing) {
          var inner = CodeMirror.innerMode(htmlMode, state.htmlState);
          if (
            (inner.mode.name == 'xml' &&
              inner.state.tagStart === null &&
              !inner.state.context &&
              inner.state.tokenize.isInText) ||
            (state.md_inside && stream.current().indexOf('>') > -1)
          ) {
            state.f = inlineNormal;
            state.block = blockNormal;
            state.htmlState = null;
          }
        }
        return style;
      }

      function local(stream, state) {
        var currListInd = state.listStack[state.listStack.length - 1] || 0;
        var hasExitedList = state.indentation < currListInd;
        var maxFencedEndInd = currListInd + 3;
        if (
          state.fencedEndRE &&
          state.indentation <= maxFencedEndInd &&
          (hasExitedList || stream.match(state.fencedEndRE))
        ) {
          if (modeCfg.highlightFormatting) state.formatting = 'code-block';
          var returnType;
          if (!hasExitedList) returnType = getType(state);
          state.localMode = state.localState = null;
          state.block = blockNormal;
          state.f = inlineNormal;
          state.fencedEndRE = null;
          state.code = 0;
          state.thisLine.fencedCodeEnd = true;
          if (hasExitedList) return switchBlock(stream, state, state.block);
          return returnType;
        } else if (state.localMode) {
          return state.localMode.token(stream, state.localState);
        } else {
          stream.skipToEnd();
          return tokenTypes.code;
        }
      }

      // Inline
      function getType(state) {
        var styles = [];

        if (state.formatting) {
          styles.push(tokenTypes.formatting);

          if (typeof state.formatting === 'string')
            state.formatting = [state.formatting];

          for (var i = 0; i < state.formatting.length; i++) {
            styles.push(tokenTypes.formatting + '-' + state.formatting[i]);

            if (state.formatting[i] === 'header') {
              styles.push(
                tokenTypes.formatting +
                  '-' +
                  state.formatting[i] +
                  '-' +
                  state.header,
              );
            }

            // Add `formatting-quote` and `formatting-quote-#` for blockquotes
            // Add `error` instead if the maximum blockquote nesting depth is passed
            if (state.formatting[i] === 'quote') {
              if (
                !modeCfg.maxBlockquoteDepth ||
                modeCfg.maxBlockquoteDepth >= state.quote
              ) {
                styles.push(
                  tokenTypes.formatting +
                    '-' +
                    state.formatting[i] +
                    '-' +
                    state.quote,
                );
              } else {
                styles.push('error');
              }
            }
          }
        }

        if (state.taskOpen) {
          styles.push('meta');
          return styles.length ? styles.join(' ') : null;
        }
        if (state.taskClosed) {
          styles.push('property');
          return styles.length ? styles.join(' ') : null;
        }

        if (state.linkHref) {
          styles.push(tokenTypes.linkHref, 'url');
        } else {
          // Only apply inline styles to non-url text
          if (state.strong) {
            styles.push(tokenTypes.strong);
          }
          if (state.em) {
            styles.push(tokenTypes.em);
          }
          if (state.strikethrough) {
            styles.push(tokenTypes.strikethrough);
          }
          if (state.emoji) {
            styles.push(tokenTypes.emoji);
          }
          if (state.linkText) {
            styles.push(tokenTypes.linkText);
          }
          if (state.code) {
            styles.push(tokenTypes.code);
          }
          if (state.image) {
            styles.push(tokenTypes.image);
          }
          if (state.imageAltText) {
            styles.push(tokenTypes.imageAltText, 'link');
          }
          if (state.imageMarker) {
            styles.push(tokenTypes.imageMarker);
          }
        }

        if (state.header) {
          styles.push(
            tokenTypes.header,
            tokenTypes.header + '-' + state.header,
          );
        }

        if (state.quote) {
          styles.push(tokenTypes.quote);

          // Add `quote-#` where the maximum for `#` is modeCfg.maxBlockquoteDepth
          if (
            !modeCfg.maxBlockquoteDepth ||
            modeCfg.maxBlockquoteDepth >= state.quote
          ) {
            styles.push(tokenTypes.quote + '-' + state.quote);
          } else {
            styles.push(tokenTypes.quote + '-' + modeCfg.maxBlockquoteDepth);
          }
        }

        if (state.list !== false) {
          var listMod = (state.listStack.length - 1) % 3;
          if (!listMod) {
            styles.push(tokenTypes.list1);
          } else if (listMod === 1) {
            styles.push(tokenTypes.list2);
          } else {
            styles.push(tokenTypes.list3);
          }
        }

        if (state.trailingSpaceNewLine) {
          styles.push('trailing-space-new-line');
        } else if (state.trailingSpace) {
          styles.push(
            'trailing-space-' + (state.trailingSpace % 2 ? 'a' : 'b'),
          );
        }

        return styles.length ? styles.join(' ') : null;
      }

      function handleText(stream, state) {
        if (stream.match(textRE, true)) {
          return getType(state);
        }
        return undefined;
      }

      function inlineNormal(stream, state) {
        var style = state.text(stream, state);
        if (typeof style !== 'undefined') return style;

        if (state.list) {
          // List marker (*, +, -, 1., etc)
          state.list = null;
          return getType(state);
        }

        if (state.taskList) {
          var taskOpen = stream.match(taskListRE, true)[1] === ' ';
          if (taskOpen) state.taskOpen = true;
          else state.taskClosed = true;
          if (modeCfg.highlightFormatting) state.formatting = 'task';
          state.taskList = false;
          return getType(state);
        }

        state.taskOpen = false;
        state.taskClosed = false;

        if (state.header && stream.match(/^#+$/, true)) {
          if (modeCfg.highlightFormatting) state.formatting = 'header';
          return getType(state);
        }

        var ch = stream.next();

        // Matches link titles present on next line
        if (state.linkTitle) {
          state.linkTitle = false;
          var matchCh = ch;
          if (ch === '(') {
            matchCh = ')';
          }
          matchCh = (matchCh + '').replace(/([.?*+^\[\]\\(){}|-])/g, '\\$1');
          var regex =
            '^\\s*(?:[^' + matchCh + '\\\\]+|\\\\\\\\|\\\\.)' + matchCh;
          if (stream.match(new RegExp(regex), true)) {
            return tokenTypes.linkHref;
          }
        }

        // If this block is changed, it may need to be updated in GFM mode
        if (ch === '`') {
          var previousFormatting = state.formatting;
          if (modeCfg.highlightFormatting) state.formatting = 'code';
          stream.eatWhile('`');
          var count = stream.current().length;
          if (state.code == 0 && (!state.quote || count == 1)) {
            state.code = count;
            return getType(state);
          } else if (count == state.code) {
            // Must be exact
            var t = getType(state);
            state.code = 0;
            return t;
          } else {
            state.formatting = previousFormatting;
            return getType(state);
          }
        } else if (state.code) {
          return getType(state);
        }

        if (ch === '\\') {
          stream.next();
          if (modeCfg.highlightFormatting) {
            var type = getType(state);
            var formattingEscape = tokenTypes.formatting + '-escape';
            return type ? type + ' ' + formattingEscape : formattingEscape;
          }
        }

        if (ch === '!' && stream.match(/\[[^\]]*\] ?(?:\(|\[)/, false)) {
          state.imageMarker = true;
          state.image = true;
          if (modeCfg.highlightFormatting) state.formatting = 'image';
          return getType(state);
        }

        if (
          ch === '[' &&
          state.imageMarker &&
          stream.match(/[^\]]*\](\(.*?\)| ?\[.*?\])/, false)
        ) {
          state.imageMarker = false;
          state.imageAltText = true;
          if (modeCfg.highlightFormatting) state.formatting = 'image';
          return getType(state);
        }

        if (ch === ']' && state.imageAltText) {
          if (modeCfg.highlightFormatting) state.formatting = 'image';
          var type = getType(state);
          state.imageAltText = false;
          state.image = false;
          state.inline = state.f = linkHref;
          return type;
        }

        if (ch === '[' && !state.image) {
          state.linkText = true;
          if (modeCfg.highlightFormatting) state.formatting = 'link';
          return getType(state);
        }

        if (ch === ']' && state.linkText) {
          if (modeCfg.highlightFormatting) state.formatting = 'link';
          var type = getType(state);
          state.linkText = false;
          state.inline = state.f = stream.match(/\(.*?\)| ?\[.*?\]/, false)
            ? linkHref
            : inlineNormal;
          return type;
        }

        if (
          ch === '<' &&
          stream.match(/^(https?|ftps?):\/\/(?:[^\\>]|\\.)+>/, false)
        ) {
          state.f = state.inline = linkInline;
          if (modeCfg.highlightFormatting) state.formatting = 'link';
          var type = getType(state);
          if (type) {
            type += ' ';
          } else {
            type = '';
          }
          return type + tokenTypes.linkInline;
        }

        if (ch === '<' && stream.match(/^[^> \\]+@(?:[^\\>]|\\.)+>/, false)) {
          state.f = state.inline = linkInline;
          if (modeCfg.highlightFormatting) state.formatting = 'link';
          var type = getType(state);
          if (type) {
            type += ' ';
          } else {
            type = '';
          }
          return type + tokenTypes.linkEmail;
        }

        if (
          modeCfg.xml &&
          ch === '<' &&
          stream.match(
            /^(!--|[a-z]+(?:\s+[a-z_:.\-]+(?:\s*=\s*[^ >]+)?)*\s*>)/i,
            false,
          )
        ) {
          var end = stream.string.indexOf('>', stream.pos);
          if (end != -1) {
            var atts = stream.string.substring(stream.start, end);
            if (/markdown\s*=\s*('|"){0,1}1('|"){0,1}/.test(atts))
              state.md_inside = true;
          }
          stream.backUp(1);
          state.htmlState = CodeMirror.startState(htmlMode);
          return switchBlock(stream, state, htmlBlock);
        }

        if (modeCfg.xml && ch === '<' && stream.match(/^\/\w*?>/)) {
          state.md_inside = false;
          return 'tag';
        } else if (ch === '*' || ch === '_') {
          var len = 1,
            before =
              stream.pos == 1 ? ' ' : stream.string.charAt(stream.pos - 2);
          while (len < 3 && stream.eat(ch)) len++;
          var after = stream.peek() || ' ';
          // See http://spec.commonmark.org/0.27/#emphasis-and-strong-emphasis
          var leftFlanking =
            !/\s/.test(after) &&
            (!punctuation.test(after) ||
              /\s/.test(before) ||
              punctuation.test(before));
          var rightFlanking =
            !/\s/.test(before) &&
            (!punctuation.test(before) ||
              /\s/.test(after) ||
              punctuation.test(after));
          var setEm = null,
            setStrong = null;
          if (len % 2) {
            // Em
            if (
              !state.em &&
              leftFlanking &&
              (ch === '*' || !rightFlanking || punctuation.test(before))
            )
              setEm = true;
            else if (
              state.em == ch &&
              rightFlanking &&
              (ch === '*' || !leftFlanking || punctuation.test(after))
            )
              setEm = false;
          }
          if (len > 1) {
            // Strong
            if (
              !state.strong &&
              leftFlanking &&
              (ch === '*' || !rightFlanking || punctuation.test(before))
            )
              setStrong = true;
            else if (
              state.strong == ch &&
              rightFlanking &&
              (ch === '*' || !leftFlanking || punctuation.test(after))
            )
              setStrong = false;
          }
          if (setStrong != null || setEm != null) {
            if (modeCfg.highlightFormatting)
              state.formatting =
                setEm == null
                  ? 'strong'
                  : setStrong == null
                  ? 'em'
                  : 'strong em';
            if (setEm === true) state.em = ch;
            if (setStrong === true) state.strong = ch;
            var t = getType(state);
            if (setEm === false) state.em = false;
            if (setStrong === false) state.strong = false;
            return t;
          }
        } else if (ch === ' ') {
          if (stream.eat('*') || stream.eat('_')) {
            // Probably surrounded by spaces
            if (stream.peek() === ' ') {
              // Surrounded by spaces, ignore
              return getType(state);
            } else {
              // Not surrounded by spaces, back up pointer
              stream.backUp(1);
            }
          }
        }

        if (modeCfg.strikethrough) {
          if (ch === '~' && stream.eatWhile(ch)) {
            if (state.strikethrough) {
              // Remove strikethrough
              if (modeCfg.highlightFormatting)
                state.formatting = 'strikethrough';
              var t = getType(state);
              state.strikethrough = false;
              return t;
            } else if (stream.match(/^[^\s]/, false)) {
              // Add strikethrough
              state.strikethrough = true;
              if (modeCfg.highlightFormatting)
                state.formatting = 'strikethrough';
              return getType(state);
            }
          } else if (ch === ' ') {
            if (stream.match(/^~~/, true)) {
              // Probably surrounded by space
              if (stream.peek() === ' ') {
                // Surrounded by spaces, ignore
                return getType(state);
              } else {
                // Not surrounded by spaces, back up pointer
                stream.backUp(2);
              }
            }
          }
        }

        if (modeCfg.emoji && ch === ':' && stream.match(/^[a-z_\d+-]+:/)) {
          state.emoji = true;
          if (modeCfg.highlightFormatting) state.formatting = 'emoji';
          var retType = getType(state);
          state.emoji = false;
          return retType;
        }

        if (ch === ' ') {
          if (stream.match(/ +$/, false)) {
            state.trailingSpace++;
          } else if (state.trailingSpace) {
            state.trailingSpaceNewLine = true;
          }
        }

        return getType(state);
      }

      function linkInline(stream, state) {
        var ch = stream.next();

        if (ch === '>') {
          state.f = state.inline = inlineNormal;
          if (modeCfg.highlightFormatting) state.formatting = 'link';
          var type = getType(state);
          if (type) {
            type += ' ';
          } else {
            type = '';
          }
          return type + tokenTypes.linkInline;
        }

        stream.match(/^[^>]+/, true);

        return tokenTypes.linkInline;
      }

      function linkHref(stream, state) {
        // Check if space, and return NULL if so (to avoid marking the space)
        if (stream.eatSpace()) {
          return null;
        }
        var ch = stream.next();
        if (ch === '(' || ch === '[') {
          state.f = state.inline = getLinkHrefInside(ch === '(' ? ')' : ']');
          if (modeCfg.highlightFormatting) state.formatting = 'link-string';
          state.linkHref = true;
          return getType(state);
        }
        return 'error';
      }

      var linkRE = {
        ')': /^(?:[^\\\(\)]|\\.|\((?:[^\\\(\)]|\\.)*\))*?(?=\))/,
        ']': /^(?:[^\\\[\]]|\\.|\[(?:[^\\\[\]]|\\.)*\])*?(?=\])/,
      };

      function getLinkHrefInside(endChar) {
        return function(stream, state) {
          var ch = stream.next();

          if (ch === endChar) {
            state.f = state.inline = inlineNormal;
            if (modeCfg.highlightFormatting) state.formatting = 'link-string';
            var returnState = getType(state);
            state.linkHref = false;
            return returnState;
          }

          stream.match(linkRE[endChar]);
          state.linkHref = true;
          return getType(state);
        };
      }

      function footnoteLink(stream, state) {
        if (stream.match(/^([^\]\\]|\\.)*\]:/, false)) {
          state.f = footnoteLinkInside;
          stream.next(); // Consume [
          if (modeCfg.highlightFormatting) state.formatting = 'link';
          state.linkText = true;
          return getType(state);
        }
        return switchInline(stream, state, inlineNormal);
      }

      function footnoteLinkInside(stream, state) {
        if (stream.match(/^\]:/, true)) {
          state.f = state.inline = footnoteUrl;
          if (modeCfg.highlightFormatting) state.formatting = 'link';
          var returnType = getType(state);
          state.linkText = false;
          return returnType;
        }

        stream.match(/^([^\]\\]|\\.)+/, true);

        return tokenTypes.linkText;
      }

      function footnoteUrl(stream, state) {
        // Check if space, and return NULL if so (to avoid marking the space)
        if (stream.eatSpace()) {
          return null;
        }
        // Match URL
        stream.match(/^[^\s]+/, true);
        // Check for link title
        if (stream.peek() === undefined) {
          // End of line, set flag to check next line
          state.linkTitle = true;
        } else {
          // More content on line, check if link title
          stream.match(
            /^(?:\s+(?:"(?:[^"\\]|\\\\|\\.)+"|'(?:[^'\\]|\\\\|\\.)+'|\((?:[^)\\]|\\\\|\\.)+\)))?/,
            true,
          );
        }
        state.f = state.inline = inlineNormal;
        return tokenTypes.linkHref + ' url';
      }

      var mode = {
        startState: function() {
          return {
            f: blockNormal,

            prevLine: { stream: null },
            thisLine: { stream: null },

            block: blockNormal,
            htmlState: null,
            indentation: 0,

            inline: inlineNormal,
            text: handleText,

            formatting: false,
            linkText: false,
            linkHref: false,
            linkTitle: false,
            code: 0,
            em: false,
            strong: false,
            header: 0,
            setext: 0,
            hr: false,
            taskList: false,
            list: false,
            listStack: [],
            quote: 0,
            trailingSpace: 0,
            trailingSpaceNewLine: false,
            strikethrough: false,
            emoji: false,
            fencedEndRE: null,
          };
        },

        copyState: function(s) {
          return {
            f: s.f,

            prevLine: s.prevLine,
            thisLine: s.thisLine,

            block: s.block,
            htmlState:
              s.htmlState && CodeMirror.copyState(htmlMode, s.htmlState),
            indentation: s.indentation,

            localMode: s.localMode,
            localState: s.localMode
              ? CodeMirror.copyState(s.localMode, s.localState)
              : null,

            inline: s.inline,
            text: s.text,
            formatting: false,
            linkText: s.linkText,
            linkTitle: s.linkTitle,
            code: s.code,
            em: s.em,
            strong: s.strong,
            strikethrough: s.strikethrough,
            emoji: s.emoji,
            header: s.header,
            setext: s.setext,
            hr: s.hr,
            taskList: s.taskList,
            list: s.list,
            listStack: s.listStack.slice(0),
            quote: s.quote,
            indentedCode: s.indentedCode,
            trailingSpace: s.trailingSpace,
            trailingSpaceNewLine: s.trailingSpaceNewLine,
            md_inside: s.md_inside,
            fencedEndRE: s.fencedEndRE,
          };
        },

        token: function(stream, state) {
          // Reset state.formatting
          state.formatting = false;

          if (stream != state.thisLine.stream) {
            state.header = 0;
            state.hr = false;

            if (stream.match(/^\s*$/, true)) {
              blankLine(state);
              return null;
            }

            state.prevLine = state.thisLine;
            state.thisLine = { stream: stream };

            // Reset state.taskList
            state.taskList = false;

            // Reset state.trailingSpace
            state.trailingSpace = 0;
            state.trailingSpaceNewLine = false;

            if (!state.localState) {
              state.f = state.block;
              if (state.f != htmlBlock) {
                var indentation = stream
                  .match(/^\s*/, true)[0]
                  .replace(/\t/g, expandedTab).length;
                state.indentation = indentation;
                state.indentationDiff = null;
                if (indentation > 0) return null;
              }
            }
          }
          return state.f(stream, state);
        },

        innerMode: function(state) {
          if (state.block == htmlBlock)
            return { state: state.htmlState, mode: htmlMode };
          if (state.localState)
            return { state: state.localState, mode: state.localMode };
          return { state: state, mode: mode };
        },

        indent: function(state, textAfter, line) {
          if (state.block == htmlBlock && htmlMode.indent)
            return htmlMode.indent(state.htmlState, textAfter, line);
          if (state.localState && state.localMode.indent)
            return state.localMode.indent(state.localState, textAfter, line);
          return CodeMirror.Pass;
        },

        blankLine: blankLine,

        getType: getType,

        closeBrackets: '()[]{}\'\'""``',
        fold: 'markdown',
      };
      return mode;
    },
    'xml',
  );

  CodeMirror.defineMIME('text/x-markdown', 'markdown');
});
